﻿using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Windows.Input;
using Telerik.Core;
using Telerik.UI.Automation.Peers;
using Windows.Foundation;
using Windows.System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Automation;
using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Markup;

namespace Telerik.UI.Xaml.Controls.Input
{
    /// <summary>
    /// Represents a control that allows for giving a rating by tapping on a RatingItem.
    /// </summary>
    [ContentProperty(Name = "Items")]
    public class RadRating : RadControl, IWeakEventListener
    {
        /// <summary>
        /// Identifies the <see cref="Value"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register(nameof(Value), typeof(double), typeof(RadRating), new PropertyMetadata(0.0, OnValueChanged));

        /// <summary>
        /// Identifies the <see cref="Orientation"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(RadRating), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));

        /// <summary>
        /// Identifies the <see cref="AutoGeneratedItemsCount"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty AutoGeneratedItemsCountProperty =
            DependencyProperty.Register(nameof(AutoGeneratedItemsCount), typeof(int), typeof(RadRating), new PropertyMetadata(5, OnAutoGeneratedItemsCountChanged));

        /// <summary>
        /// Identifies the <see cref="EmptyIconContentTemplate"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty EmptyIconContentTemplateProperty =
            DependencyProperty.Register(nameof(EmptyIconContentTemplate), typeof(DataTemplate), typeof(RadRating), new PropertyMetadata(null, OnEmptyIconContentTemplateChanged));

        /// <summary>
        /// Identifies the <see cref="FilledIconContentTemplate"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty FilledIconContentTemplateProperty =
            DependencyProperty.Register(nameof(FilledIconContentTemplate), typeof(DataTemplate), typeof(RadRating), new PropertyMetadata(null, OnFilledIconContentTemplateChanged));

        /// <summary>
        /// Identifies the <see cref="EmptyIconContent"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty EmptyIconContentProperty =
            DependencyProperty.Register(nameof(EmptyIconContent), typeof(object), typeof(RadRating), new PropertyMetadata(null, OnEmptyIconContentChanged));

        /// <summary>
        /// Identifies the <see cref="FilledIconContent"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty FilledIconContentProperty =
            DependencyProperty.Register(nameof(FilledIconContent), typeof(object), typeof(RadRating), new PropertyMetadata(null, OnFilledIconContentChanged));

        /// <summary>
        /// Identifies the <see cref="EmptyIconStyle"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty EmptyIconStyleProperty =
            DependencyProperty.Register(nameof(EmptyIconStyle), typeof(Style), typeof(RadRating), new PropertyMetadata(null, OnEmptyIconStyleChanged));

        /// <summary>
        /// Identifies the <see cref="FilledIconStyle"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty FilledIconStyleProperty =
            DependencyProperty.Register(nameof(FilledIconStyle), typeof(Style), typeof(RadRating), new PropertyMetadata(null, OnFilledIconStyleChanged));

        /// <summary>
        /// Identifies the <see cref="HighlightedIconStyle"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty HighlightedIconStyleProperty =
            DependencyProperty.Register(nameof(HighlightedIconStyle), typeof(Style), typeof(RadRating), new PropertyMetadata(null, OnHighlightedIconStyleChanged));

        /// <summary>
        /// Identifies the <see cref="IsReadOnly"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty IsReadOnlyProperty =
            DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(RadRating), new PropertyMetadata(false));

        /// <summary>
        /// Identifies the <see cref="IsPanEnabled"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty IsPanEnabledProperty =
            DependencyProperty.Register(nameof(IsPanEnabled), typeof(bool), typeof(RadRating), new PropertyMetadata(true));

        /// <summary>
        /// Identifies the <see cref="RatingSelectionMode"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty RatingSelectionModeProperty =
            DependencyProperty.Register(nameof(RatingSelectionMode), typeof(RatingSelectionMode), typeof(RadRating), new PropertyMetadata(RatingSelectionMode.Continuous, OnRatingSelectionModeChanged));

        /// <summary>
        /// Identifies the <see cref="RatingDisplayPrecision"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty RatingDisplayPrecisionProperty =
            DependencyProperty.Register(nameof(RatingDisplayPrecision), typeof(RatingDisplayPrecision), typeof(RadRating), new PropertyMetadata(RatingDisplayPrecision.Exact, OnRatingDisplayPrecisionChanged));

        /// <summary>
        /// Identifies the <see cref="Command"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.Register(nameof(Command), typeof(System.Windows.Input.ICommand), typeof(RadRating), new PropertyMetadata(null, OnCommandChanged));

        /// <summary>
        /// Identifies the <see cref="CommandParameter"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(RadRating), new PropertyMetadata(null, OnCommandParameterChanged));

        // exposed for testing purposes
        internal StackPanel ratingPanel;

        private const string AutoGeneratedItemsCountExceptionMessage = "AutoGeneratedItemsCount can not be set when there are manually added items. Clear the Items collection first.";
        private const string ItemsCollectionChangedExceptionMessage = "Items collection can not be changed when the AutoGeneratedItemsCount property is set.";
        private const int RoundingValuePrecision = 5;
        private const double VelocityThreshold = 0.05;

        private readonly ObservableCollection<RadRatingItem> itemsSource;
        private WeakEventHandler<EventArgs> canExecuteChangedHandler;
        private bool isItemsSourceChangedSilently;
        private bool settingValuePropertySilently;
        private bool freezeDisplayValue;
        private double displayValue;
        private bool isInHighlightMode = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="RadRating" /> class.
        /// </summary>
        public RadRating()
        {
            this.DefaultStyleKey = typeof(RadRating);
            this.itemsSource = new ObservableCollection<RadRatingItem>();
            this.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
            this.itemsSource.CollectionChanged += this.HandleItemsSourceCollectionChanged;
        }

        /// <summary>
        /// Finalizes an instance of the <see cref="RadRating"/> class.
        /// </summary>
        ~RadRating()
        {
            if (this.canExecuteChangedHandler != null)
            {
                this.canExecuteChangedHandler.Unsubscribe();
            }
        }

        /// <summary>
        /// Occurs when the <see cref="Value"/> property is changed.
        /// </summary>
        public event EventHandler<ValueChangedEventArgs<object>> ValueChanged;

        /// <summary>
        /// Occurs when <see cref="Value"/> property is changing.
        /// </summary>
        public event EventHandler<ValueChangingEventArgs<object>> ValueChanging;

        private enum Location
        {
            Top = 0,
            Left = 1,
            Bottom = 2,
            Right = 4
        }

        /// <summary>
        /// Gets or sets the command parameter for the <see cref="Command"/>.
        /// </summary>
        public object CommandParameter
        {
            get
            {
                return this.GetValue(CommandParameterProperty);
            }
            set
            {
                this.SetValue(CommandParameterProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the command executed when the <see cref="Value"/> is changed.
        /// </summary>
        public System.Windows.Input.ICommand Command
        {
            get
            {
                return (System.Windows.Input.ICommand)this.GetValue(CommandProperty);
            }
            set
            {
                this.SetValue(CommandProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the rating display precision.
        /// </summary>
        public RatingDisplayPrecision RatingDisplayPrecision
        {
            get
            {
                return (RatingDisplayPrecision)this.GetValue(RatingDisplayPrecisionProperty);
            }
            set
            {
                this.SetValue(RatingDisplayPrecisionProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the rating selection mode.
        /// </summary>
        public RatingSelectionMode RatingSelectionMode
        {
            get
            {
                return (RatingSelectionMode)this.GetValue(RatingSelectionModeProperty);
            }
            set
            {
                this.SetValue(RatingSelectionModeProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the rating control responds to pan gesture.
        /// </summary>
        public bool IsPanEnabled
        {
            get
            {
                return (bool)this.GetValue(IsPanEnabledProperty);
            }
            set
            {
                this.SetValue(IsPanEnabledProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the rating can be changed or is read only.
        /// </summary>
        public bool IsReadOnly
        {
            get
            {
                return (bool)this.GetValue(IsReadOnlyProperty);
            }
            set
            {
                this.SetValue(IsReadOnlyProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the rating value.
        /// </summary>
        public double Value
        {
            get
            {
                return (double)this.GetValue(ValueProperty);
            }
            set
            {
                this.SetValue(ValueProperty, value);

                // raise the change for UIA 
                if (AutomationPeer.ListenerExists(AutomationEvents.PropertyChanged))
                {
                    RadRatingAutomationPeer peer = FrameworkElementAutomationPeer.FromElement(this) as RadRatingAutomationPeer;
                    if (peer != null)
                    {
                        peer.RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, this.GetValue(ValueProperty), value);
                        peer.RaisePropertyChangedEvent(RangeValuePatternIdentifiers.ValueProperty, this.GetValue(ValueProperty), value);
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the style of the <see cref="RadRatingItem"/> icon content in highlighted state.
        /// </summary>
        public Style HighlightedIconStyle
        {
            get
            {
                return (Style)this.GetValue(HighlightedIconStyleProperty);
            }
            set
            {
                this.SetValue(HighlightedIconStyleProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the style of the <see cref="RadRatingItem"/> icon content when the item is not selected.
        /// </summary>
        public Style EmptyIconStyle
        {
            get
            {
                return (Style)this.GetValue(EmptyIconStyleProperty);
            }
            set
            {
                this.SetValue(EmptyIconStyleProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the style of the <see cref="RadRatingItem"/> icon content when the item is selected.
        /// </summary>
        public Style FilledIconStyle
        {
            get
            {
                return (Style)this.GetValue(FilledIconStyleProperty);
            }
            set
            {
                this.SetValue(FilledIconStyleProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the content of the <see cref="RadRatingItem"/> icon when the item is not selected.
        /// </summary>
        public object EmptyIconContent
        {
            get
            {
                return this.GetValue(EmptyIconContentProperty);
            }
            set
            {
                this.SetValue(EmptyIconContentProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the content of the <see cref="RadRatingItem"/> icon when the item is selected.
        /// </summary>
        public object FilledIconContent
        {
            get
            {
                return this.GetValue(FilledIconContentProperty);
            }
            set
            {
                this.SetValue(FilledIconContentProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the content template of the <see cref="RadRatingItem"/> when the item is not selected.
        /// </summary>
        public object EmptyIconContentTemplate
        {
            get
            {
                return (DataTemplate)this.GetValue(EmptyIconContentTemplateProperty);
            }
            set
            {
                this.SetValue(EmptyIconContentTemplateProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the content template of the <see cref="RadRatingItem"/> when the item is selected.
        /// </summary>
        public object FilledIconContentTemplate
        {
            get
            {
                return this.GetValue(FilledIconContentTemplateProperty);
            }
            set
            {
                this.SetValue(FilledIconContentTemplateProperty, value);
            }
        }

        /// <summary>
        /// Gets or sets the count of visual items that are automatically created. Can not be used when rating items are added manually.
        /// </summary>
        public int AutoGeneratedItemsCount
        {
            get
            {
                return (int)this.GetValue(AutoGeneratedItemsCountProperty);
            }
            set
            {
                this.SetValue(AutoGeneratedItemsCountProperty, value);
            }
        }

        /// <summary>
        /// Gets the rating items.
        /// </summary>
        public ObservableCollection<RadRatingItem> Items
        {
            get
            {
                return this.itemsSource;
            }
        }

        /// <summary>
        /// Gets or sets the rating orientation.
        /// </summary>
        public Orientation Orientation
        {
            get
            {
                return (Orientation)this.GetValue(OrientationProperty);
            }
            set
            {
                this.SetValue(OrientationProperty, value);
            }
        }

        internal StackPanel RatingPanel
        {
            get
            {
                return this.ratingPanel;
            }
        }

        private bool IsItemsSourceManual
        {
            get
            {
                return this.AutoGeneratedItemsCount == 0 || this.ReadLocalValue(AutoGeneratedItemsCountProperty) == DependencyProperty.UnsetValue;
            }
        }

        void IWeakEventListener.ReceiveEvent(object sender, object args)
        {
            this.UpdateIsReadOnlyFromCommand();
        }

        internal void HandleTapEvent(RadRatingItem ratingItem)
        {
            int index = this.GetIndexOf(ratingItem);
            if (index == -1)
            {
                Debug.Assert(false, "Unknown item");
                return;
            }

            this.Value = index + 1;
        }

        internal int GetIndexOf(RadRatingItem ratingItem)
        {
            return this.itemsSource.IndexOf(ratingItem);     
        }

        /// <inheritdoc/>
        protected override bool ApplyTemplateCore()
        {
            this.ratingPanel = this.GetTemplateChild("Part_RatingPanel") as StackPanel;
            if (this.ratingPanel == null)
            {
                throw new MissingTemplatePartException("Part_RatingPanel", typeof(StackPanel));
            }

            if (this.itemsSource.Count != 0)
            {
                if (!this.IsItemsSourceManual)
                {
                    throw new InvalidOperationException(AutoGeneratedItemsCountExceptionMessage);
                }

                this.SetValue(AutoGeneratedItemsCountProperty, 0);
            }
            else
            {
                if (this.AutoGeneratedItemsCount == (int)AutoGeneratedItemsCountProperty.GetMetadata(typeof(int)).DefaultValue)
                {
                    this.SetValue(AutoGeneratedItemsCountProperty, (int)AutoGeneratedItemsCountProperty.GetMetadata(typeof(int)).DefaultValue);
                }

                this.GenerateRatingItems();
            }

            this.PopulateRatingPanel();

            double coercedValue = RadMath.CoerceValue(this.Value, 0, this.itemsSource.Count);
            if (this.Value != coercedValue)
            {
                this.SetValuePropertySilently(ValueProperty, coercedValue);
            }

            double defaultValue = (double)ValueProperty.GetMetadata(typeof(double)).DefaultValue;
            if (this.Value != defaultValue)
            {
                this.HandleValueChange(defaultValue, this.Value);
            }

            this.UpdateDisplayValue();
            this.UpdateFillRatio();
            this.UpdateVisualState(false);

            return base.ApplyTemplateCore();
        }

        /// <inheritdoc/>
        protected virtual void OnValueChanged(ValueChangedEventArgs<object> args)
        {
            if (this.ValueChanged != null)
            {
                this.ValueChanged(this, args);
            }
        }

        /// <inheritdoc/>
        protected virtual void OnValueChanging(ValueChangingEventArgs<object> args)
        {
            if (this.ValueChanging != null)
            {
                this.ValueChanging(this, args);
            }
        }

        /// <inheritdoc/>
        protected override void OnManipulationStarted(ManipulationStartedRoutedEventArgs args)
        {
            base.OnManipulationStarted(args);

            if (this.IsReadOnly || !this.IsPanEnabled)
            {
                return;
            }

            this.SetHighlightMode(true);
            this.HandlePointManipulation(args.Position);
        }

        /// <inheritdoc/>
        protected override void OnKeyDown(KeyRoutedEventArgs e)
        {
            if (e.Handled)
            {
                return;
            }

            if (!this.IsReadOnly)
            {
                bool handled = false;

                switch (e.Key)
                {
                    case VirtualKey.Left:
                    case VirtualKey.Down:
                        this.Value--;
                        handled = true;
                        break;
                    case VirtualKey.Right:
                    case VirtualKey.Up:
                        this.Value++;
                        break;
                    case VirtualKey.Home:
                        this.Value = 0.0;
                        handled = true;
                        break;
                    case VirtualKey.End:
                        this.Value = this.Items.Count;
                        handled = true;
                        break;
                }

                e.Handled = handled;
            }
        }

        /// <inheritdoc/>
        protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs args)
        {
            base.OnManipulationDelta(args);

            if (this.IsReadOnly || !this.IsPanEnabled)
            {
                return;
            }

            args.Handled = true;
            this.HandlePanManipulation(args);
        }

        /// <inheritdoc/>
        protected override void OnManipulationCompleted(ManipulationCompletedRoutedEventArgs args)
        {
            base.OnManipulationCompleted(args);

            if (this.IsReadOnly || !this.IsPanEnabled)
            {
                return;
            }

            args.Handled = true;
            this.SetHighlightMode(false);

            var rectangle = new RadRect(args.Container.RenderSize.Width, args.Container.RenderSize.Height);
            if (this.freezeDisplayValue || rectangle.Contains(args.Position.X, args.Position.Y))
            {
                this.Value = this.displayValue;
                this.freezeDisplayValue = false;
            }
            else
            {
                this.UpdateDisplayValue();
                this.UpdateFillRatio();
            }
        }

        /// <inheritdoc/>
        protected override AutomationPeer OnCreateAutomationPeer()
        {
            return new RadRatingAutomationPeer(this);
        }

        private static void OnCommandChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            RadRating rating = sender as RadRating;

            if (rating.canExecuteChangedHandler != null)
            {
                rating.canExecuteChangedHandler.Unsubscribe();
            }

            System.Windows.Input.ICommand command = args.NewValue as System.Windows.Input.ICommand;
            if (command != null)
            {
                rating.canExecuteChangedHandler = new WeakEventHandler<EventArgs>(command, rating, KnownEvents.CanExecuteChanged);
                rating.UpdateIsReadOnlyFromCommand();
            }
        }

        private static void OnCommandParameterChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            RadRating rating = sender as RadRating;
            rating.UpdateIsReadOnlyFromCommand();
        }

        private static void OnRatingDisplayPrecisionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            RadRating rating = sender as RadRating;
            rating.UpdateDisplayValue();
            rating.UpdateFillRatio();
        }

        private static void OnRatingSelectionModeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            RadRating rating = sender as RadRating;
            rating.UpdateFillRatio();
        }

        private static void OnFilledIconStyleChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var rating = sender as RadRating;
            if (!rating.IsTemplateApplied)
            {
                return;
            }

            foreach (var item in rating.itemsSource)
            {
                item.FilledIconStyle = (Style)args.NewValue;
            }
        }

        private static void OnHighlightedIconStyleChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var rating = sender as RadRating;

            if (!rating.IsTemplateApplied)
            {
                return;
            }

            foreach (var item in rating.itemsSource)
            {
                item.HighlightedIconStyle = (Style)args.NewValue;
            }
        }

        private static void OnEmptyIconStyleChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var rating = sender as RadRating;

            if (!rating.IsTemplateApplied)
            {
                return;
            }
            foreach (var item in rating.itemsSource)
            {
                item.EmptyIconStyle = (Style)args.NewValue;
            }
        }

        private static void OnFilledIconContentTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var rating = sender as RadRating;

            if (!rating.IsTemplateApplied)
            {
                return;
            }
            foreach (var item in rating.itemsSource)
            {
                item.FilledIconContentTemplate = args.NewValue;
            }
        }

        private static void OnFilledIconContentChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var rating = sender as RadRating;

            if (!rating.IsTemplateApplied)
            {
                return;
            }
            foreach (var item in rating.itemsSource)
            {
                item.FilledIconContent = args.NewValue;
            }
        }

        private static void OnEmptyIconContentTemplateChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var rating = sender as RadRating;

            if (!rating.IsTemplateApplied)
            {
                return;
            }
            foreach (var item in rating.itemsSource)
            {
                item.EmptyIconContentTemplate = args.NewValue;
            }
        }

        private static void OnEmptyIconContentChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var rating = sender as RadRating;

            if (!rating.IsTemplateApplied)
            {
                return;
            }
            foreach (var item in rating.itemsSource)
            {
                item.EmptyIconContent = args.NewValue;
            }
        }

        private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            RadRating rating = sender as RadRating;

            if (rating.settingValuePropertySilently)
            {
                return;
            }

            if (!rating.IsTemplateApplied)
            {
                return;
            }

            double oldValue = (double)args.OldValue;
            double newValue = RadMath.CoerceValue((double)args.NewValue, 0, rating.itemsSource.Count);

            if (newValue != rating.Value)
            {
                rating.SetValuePropertySilently(ValueProperty, newValue);
            }

            if (newValue != oldValue)
            {
                rating.HandleValueChange(oldValue, newValue);
            }
        }

        private static void OnAutoGeneratedItemsCountChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            RadRating rating = sender as RadRating;

            if (!rating.IsTemplateApplied)
            {
                return;
            }

            if ((int)args.OldValue == 0 && rating.itemsSource.Count != 0)
            {
                throw new InvalidOperationException(AutoGeneratedItemsCountExceptionMessage);
            }

            rating.isItemsSourceChangedSilently = true;
            rating.itemsSource.Clear();
            int newValue = (int)args.NewValue;

            if (newValue <= 0)
            {
                rating.AutoGeneratedItemsCount = 0;
            }
            else
            {
                rating.GenerateRatingItems();

                double coercedValue = RadMath.CoerceValue(rating.Value, 0, rating.itemsSource.Count);
                if (rating.Value != coercedValue)
                {
                    rating.SetValuePropertySilently(ValueProperty, coercedValue);
                    rating.UpdateDisplayValue();
                    rating.UpdateFillRatio();
                }
            }

            rating.isItemsSourceChangedSilently = false;
            rating.PopulateRatingPanel();
        }

        private static void OnOrientationChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var rating = sender as RadRating;

            if (!rating.IsTemplateApplied)
            {
                return;
            }

            rating.ratingPanel.Orientation = (Orientation)args.NewValue;
            rating.PopulateRatingPanel();
        }

        private void HandleItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (!this.IsTemplateApplied || this.isItemsSourceChangedSilently)
            {
                return;
            }

            if (!this.IsItemsSourceManual)
            {
                throw new InvalidOperationException(ItemsCollectionChangedExceptionMessage);
            }

            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    this.AddItemOnCollectionChanged(e.NewItems[0] as RadRatingItem, e.NewStartingIndex);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    this.RemoveItemOnCollectionChanged(e.OldStartingIndex);
                    break;
                case NotifyCollectionChangedAction.Reset:
                    this.ResetItems();
                    break;
                case NotifyCollectionChangedAction.Move:
                    this.MoveItemOnCollectionChanged((uint)e.OldStartingIndex, (uint)e.NewStartingIndex);
                    break;
                case NotifyCollectionChangedAction.Replace:
                    RadRatingItem oldItem = e.OldItems[0] as RadRatingItem;
                    RadRatingItem newItem = e.NewItems[0] as RadRatingItem;
                    this.ReplaceItemsOnCollectionChanged(oldItem, newItem);
                    break;
            }
        }

        private void MoveItemOnCollectionChanged(uint oldIndex, uint newIndex)
        {
            this.ratingPanel.Children.Move(oldIndex, newIndex);
        }

        private void ReplaceItemsOnCollectionChanged(RadRatingItem oldItem, RadRatingItem newItem)
        {
            oldItem.Owner = null;
            int index = this.ratingPanel.Children.IndexOf(oldItem);
            this.ratingPanel.Children.RemoveAt(index);
            newItem.Owner = this;
            this.ratingPanel.Children.Insert(index, newItem);

            this.UpdateFillRatio();
        }

        private void ResetItems()
        {
            foreach (RadRatingItem item in this.ratingPanel.Children)
            {
                item.Owner = null;
            }

            this.ratingPanel.Children.Clear();
        }

        private void RemoveItemOnCollectionChanged(int index)
        {
            var ratingItem = this.ratingPanel.Children[index] as RadRatingItem;
            ratingItem.Owner = null;
            this.ratingPanel.Children.Remove(ratingItem);

            if (this.itemsSource.Count < this.Value)
            {
                this.SetValuePropertySilently(RadRating.ValueProperty, (double)this.itemsSource.Count);
                this.UpdateDisplayValue();
            }

            this.UpdateFillRatio();
        }

        private void AddItemOnCollectionChanged(RadRatingItem ratingItem, int index)
        {
            ratingItem.Owner = this;
            this.SetUnsetItemProperties(ratingItem);
            if (this.Orientation == Orientation.Vertical)
            {
                index = this.ratingPanel.Children.Count - index;
            }

            this.ratingPanel.Children.Insert(index, ratingItem);
            this.UpdateFillRatio();
        }

        private void UpdateIsReadOnlyFromCommand()
        {
            ICommand command = this.Command;
            if (command != null)
            {
                this.IsReadOnly = !command.CanExecute(this.CommandParameter);
            }
        }

        private void HandlePanManipulation(ManipulationDeltaRoutedEventArgs args)
        {
            var rectangle = new RadRect(args.Container.RenderSize.Width, args.Container.RenderSize.Height);
            var point = args.Position;

            var x = point.X;
            var y = point.Y;

            var dx = args.Delta.Translation.X;
            var dy = args.Delta.Translation.Y;

            var manipulationStartPointInside = rectangle.Contains(x, y);
            var manipulationEndPointInside = rectangle.Contains(x + dx, y + dy);

            if (!manipulationStartPointInside && !manipulationEndPointInside)
            {
                // pointer was outside during the whole manipulation, nothing should happen
                return;
            }

            if (manipulationEndPointInside)
            {
                // at the end the pointer is inside the container => handle point manipulation
                this.HandlePointManipulation(new Point(x + dx, y + dx));
            }
            else
            {
                // the pointer has left the contained during the manipulation - should determine the direction
                var manipulationLeavingPointDirection = this.GetManipulationDirection(dy, dx, y, x, rectangle);

                this.HandleManipulationLeavesRating(manipulationLeavingPointDirection);

                if (this.freezeDisplayValue)
                {
                    this.UpdateFillRatio();
                }
            }
        }

        private void HandleManipulationLeavesRating(Location manipulationLeavingPointDirection)
        {
            if (this.Orientation == Orientation.Horizontal)
            {
                switch (manipulationLeavingPointDirection)
                {
                    case Location.Top:
                    case Location.Bottom:
                        this.freezeDisplayValue = false;
                        break;
                    case Location.Left:
                        this.displayValue = this.itemsSource.Count;
                        this.freezeDisplayValue = true;
                        break;
                    case Location.Right:
                        this.displayValue = 0;
                        this.freezeDisplayValue = true;
                        break;
                }
            }
            else
            {
                switch (manipulationLeavingPointDirection)
                {
                    case Location.Left:
                    case Location.Right:
                        this.freezeDisplayValue = false;
                        break;
                    case Location.Bottom:
                        this.displayValue = 0;
                        this.freezeDisplayValue = true;
                        break;
                    case Location.Top:
                        this.displayValue = this.itemsSource.Count;
                        this.freezeDisplayValue = true;
                        break;
                }
            }
        }

        private Location GetManipulationDirection(double dy, double dx, double y0, double x0, RadRect rectangle)
        {
            var angle = Math.Atan2(dy, dx);

            var first = Math.Atan2(0 - y0, 0 - x0);
            var second = Math.Atan2(0 - y0, rectangle.Width - x0);
            var third = Math.Atan2(rectangle.Height - y0, rectangle.Width - x0);
            var forth = Math.Atan2(rectangle.Height - y0, 0 - x0);

            if (angle < first || angle > forth)
            {
                return Location.Right;
            }
            else if (angle < second)
            {
                return Location.Top;
            }
            else if (angle < third)
            {
                return Location.Left;
            }
            else
            {
                // first < angle , angle < forth
                return Location.Bottom;
            }
        }

        private void HandlePointManipulation(Point interactionPoint)
        {
            for (int i = 0; i < this.itemsSource.Count; i++)
            {
                if (this.itemsSource[i].HitTest(interactionPoint))
                {
                    this.displayValue = i + 1;
                    this.UpdateFillRatio();
                    return;
                }
            }
        }

        private void SetHighlightMode(bool value)
        {
            if (this.isInHighlightMode == value)
            {
                return;
            }

            this.isInHighlightMode = value;
            foreach (RadRatingItem ratingItem in this.itemsSource)
            {
                ratingItem.SetHighlightMode(value);
            }
        }

        private void UpdateDisplayValue()
        {
            double displayValue = this.Value;

            if (this.RatingDisplayPrecision == RatingDisplayPrecision.Half)
            {
                double doubleDisplayValue = displayValue * 2;
                double roundedDoubleDisplayValue = Math.Round(doubleDisplayValue);
                displayValue = roundedDoubleDisplayValue / 2;
            }
            else if (this.RatingDisplayPrecision == RatingDisplayPrecision.Item)
            {
                displayValue = Math.Round(displayValue);
            }

            this.displayValue = displayValue;
        }

        private void UpdateFillRatio()
        {
            int roundedDisplayValue = (int)this.displayValue;
            double lastItemFillRatio = this.displayValue - roundedDisplayValue;
            lastItemFillRatio = Math.Round(lastItemFillRatio, RoundingValuePrecision);
            int itemsCount = this.itemsSource.Count;

            if (this.RatingSelectionMode == RatingSelectionMode.Single)
            {
                for (int i = 0; i < itemsCount; i++)
                {
                    this.itemsSource[i].FillRatio = 0;
                }
                if (lastItemFillRatio > 0)
                {
                    if (roundedDisplayValue < itemsCount)
                    {
                        this.itemsSource[roundedDisplayValue].FillRatio = lastItemFillRatio;
                    }
                }
                else
                {
                    if (roundedDisplayValue > 0)
                    {
                        if (roundedDisplayValue - 1 < itemsCount)
                        {
                            this.itemsSource[roundedDisplayValue - 1].FillRatio = 1;
                        }
                    }
                }
            }
            else
            {
                for (int i = 0; i < roundedDisplayValue && i < itemsCount; i++)
                {
                    this.itemsSource[i].FillRatio = 1;
                }
                if (roundedDisplayValue < itemsCount)
                {
                    this.itemsSource[roundedDisplayValue].FillRatio = lastItemFillRatio;
                }
                for (int i = roundedDisplayValue + 1; i < itemsCount; i++)
                {
                    this.itemsSource[i].FillRatio = 0;
                }
            }
        }

        private void GenerateRatingItems()
        {
            for (int i = 0; i < this.AutoGeneratedItemsCount; i++)
            {
                RadRatingItem item = new RadRatingItem { Owner = this };
                this.SetUnsetItemProperties(item);
                this.itemsSource.Add(item);
            }
        }

        private void PopulateRatingPanel()
        {
            this.ResetItems();

            foreach (RadRatingItem item in this.itemsSource)
            {
                this.SetUnsetItemProperties(item);

                if (this.ratingPanel.Orientation == Orientation.Horizontal)
                {
                    this.ratingPanel.Children.Add(item);
                }
                else
                {
                    this.ratingPanel.Children.Insert(0, item);
                }
            }
        }

        private void SetUnsetItemProperties(RadRatingItem item)
        {
            if (item.ReadLocalValue(RadRatingItem.EmptyIconContentProperty) == DependencyProperty.UnsetValue)
            {
                item.EmptyIconContent = this.EmptyIconContent;
            }
            if (item.ReadLocalValue(RadRatingItem.EmptyIconContentTemplateProperty) == DependencyProperty.UnsetValue)
            {
                item.EmptyIconContentTemplate = this.EmptyIconContentTemplate;
            }
            if (item.ReadLocalValue(RadRatingItem.EmptyIconStyleProperty) == DependencyProperty.UnsetValue)
            {
                item.EmptyIconStyle = this.EmptyIconStyle;
            }
            if (item.ReadLocalValue(RadRatingItem.FilledIconContentProperty) == DependencyProperty.UnsetValue)
            {
                item.FilledIconContent = this.FilledIconContent;
            }
            if (item.ReadLocalValue(RadRatingItem.FilledIconContentTemplateProperty) == DependencyProperty.UnsetValue)
            {
                item.FilledIconContentTemplate = this.FilledIconContentTemplate;
            }
            if (item.ReadLocalValue(RadRatingItem.FilledIconStyleProperty) == DependencyProperty.UnsetValue)
            {
                item.FilledIconStyle = this.FilledIconStyle;
            }
            if (item.ReadLocalValue(RadRatingItem.HighlightedIconStyleProperty) == DependencyProperty.UnsetValue)
            {
                item.HighlightedIconStyle = this.HighlightedIconStyle;
            }
            if (item.ReadLocalValue(RadRatingItem.FontSizeProperty) == DependencyProperty.UnsetValue)
            {
                item.FontSize = this.FontSize;
            }
        }

        private void SetValuePropertySilently(DependencyProperty property, object value)
        {
            this.settingValuePropertySilently = true;
            this.SetValue(property, value);
            this.settingValuePropertySilently = false;
        }

        private void HandleValueChange(double oldValue, double newValue)
        {
            double roundedNewValue = Math.Round(newValue, RoundingValuePrecision);
            if (roundedNewValue != newValue)
            {
                this.SetValuePropertySilently(ValueProperty, roundedNewValue);
            }

            ValueChangingEventArgs<object> valueChangingArgs = new ValueChangingEventArgs<object>(oldValue, roundedNewValue);
            this.OnValueChanging(valueChangingArgs);
            if (!valueChangingArgs.Cancel)
            {
                this.UpdateDisplayValue();
                this.UpdateFillRatio();
                this.OnValueChanged(new ValueChangedEventArgs<object>(oldValue, roundedNewValue));
                this.RaiseAutomationEvent(oldValue, roundedNewValue);
                this.RaiseCommandExecute();
            }
            else
            {
                this.SetValuePropertySilently(RadRating.ValueProperty, oldValue);
            }
        }

        private void RaiseAutomationEvent(double oldValue, double newValue)
        {
            AutomationPeer peer = FrameworkElementAutomationPeer.FromElement(this);
            if (peer != null)
            {
                peer.RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, oldValue, newValue);

                var ratingItem = this.Items.ElementAtOrDefault((int)newValue - 1) as RadRatingItem;
                if (ratingItem != null)
                {
                    var ratingItemPeer = FrameworkElementAutomationPeer.FromElement(ratingItem);
                    if (ratingItemPeer != null)
                    {
                        ratingItemPeer.RaiseAutomationEvent(AutomationEvents.AutomationFocusChanged);
                    }
                }
            }
        }

        private void RaiseCommandExecute()
        {
            System.Windows.Input.ICommand command = this.Command;
            if (command != null)
            {
                object parameter = this.ReadLocalValue(RadRating.CommandParameterProperty);
                if (parameter == DependencyProperty.UnsetValue)
                {
                    parameter = this.Value;
                }
                else
                {
                    parameter = this.GetValue(RadRating.CommandParameterProperty);
                }

                if (command.CanExecute(parameter))
                {
                    command.Execute(parameter);
                }
            }
        }
    }
}